/**
 * Copyright (C) 2013- Iordan Iordanov
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */


package com.undatech.opaque.input;

import android.content.Context;
import android.os.Handler;
import android.os.SystemClock;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;

import com.undatech.opaque.RemoteCanvas;
import com.undatech.opaque.SpiceCommunicator;

public abstract class RemoteKeyboard {
    private static final String TAG = "RemoteKeyboard";

	public final static int SCANCODE_LCTRL = 29;
	public final static int SCANCODE_RCTRL = 97;
	
    // My mask for the modifier masks.
    public final static int SHIFT_ON_MASK = 1;
    public final static int ALT_ON_MASK   = 2;
    public final static int CTRL_ON_MASK  = 4;
    public final static int SUPER_ON_MASK = 8;
    public final static int ALTGR_ON_MASK = 16;
    
	protected RemoteCanvas canvas;
	protected Handler handler;
	protected SpiceCommunicator spicecomm;
	protected Context context;
	protected KeyRepeater keyRepeater;

	// Variable holding the state of any pressed hardware meta keys
	protected int hardwareMetaState = 0;
	
	// Variable holding the state of the on-screen meta keys
	protected int onScreenMetaState = 0;
	
    // This variable tells us whether we need to skip junk characters for
    // SDK >= 16 and LatinIME next time a multi-character event comes along.
    public boolean skippedJunkChars = true;
    
    // Variable used for BB10 workarounds
    boolean bb = false;
    
    // Variable holding the state of the last metaState sent over with
    // a button down event. It is reset to 0 with a button up event.
    protected int lastDownMetaState = 0;
    
	RemoteKeyboard (SpiceCommunicator r, RemoteCanvas v, Handler h) {
		spicecomm = r;
		canvas = v;
		handler = h;
		keyRepeater = new KeyRepeater (this, h);
		
        if (android.os.Build.MODEL.contains("BlackBerry") ||
            android.os.Build.BRAND.contains("BlackBerry") || 
            android.os.Build.MANUFACTURER.contains("BlackBerry")) {
            bb = true;
        }
	}

	/**
	 * Sends key event to server with no additional meta state.
	 * @param keyCode
	 * @param event
	 * @return
	 */
	public abstract boolean keyEvent(int keyCode, KeyEvent event);

	/**
	 * Sends key event with additional meta state to server
	 * @param keyCode
	 * @param event
	 * @param additionalMetaState
	 * @return
	 */
	public abstract boolean keyEvent(int keyCode, KeyEvent event, int additionalMetaState);

	/**
	 * Starts repeating a key event.
	 * @param keyCode
	 * @param event
	 */
	public void repeatKeyEvent(int keyCode, KeyEvent event) { keyRepeater.start(keyCode, event); }

	/**
	 * Stops repeating the last key event being repeated.
	 */
	public void stopRepeatingKeyEvent() { keyRepeater.stop(); }
	
	/**
	 * Toggles on-screen Ctrl mask. Returns true if result is Ctrl enabled, false otherwise.
	 * @return true if on false otherwise.
	 */
	public boolean onScreenCtrlToggle()	{
		// If we find Ctrl on, turn it off. Otherwise, turn it on.
		if (onScreenMetaState == (onScreenMetaState | CTRL_ON_MASK)) {
			onScreenCtrlOff();
			return false;
		}
		else {
			onScreenCtrlOn();
			return true;
		}
	}

	/**
	 * Turns on on-screen Ctrl.
	 */
	public void onScreenCtrlOn()	{
		onScreenMetaState = onScreenMetaState | CTRL_ON_MASK;
	}
	
	/**
	 * Turns off on-screen Ctrl.
	 */
	public void onScreenCtrlOff()	{
		onScreenMetaState = onScreenMetaState & ~CTRL_ON_MASK;
	}
	
	/**
	 * Toggles on-screen Alt mask.  Returns true if result is Alt enabled, false otherwise.
	 * @return true if on false otherwise.
	 */
	public boolean onScreenAltToggle() {
		// If we find Alt on, turn it off. Otherwise, turn it on.
		if (onScreenMetaState == (onScreenMetaState | ALT_ON_MASK)) {
			onScreenAltOff();
			return false;
		}
		else {
			onScreenAltOn();
			return true;
		}
	}

	/**
	 * Turns on on-screen Alt.
	 */
	public void onScreenAltOn()	{
		onScreenMetaState = onScreenMetaState | ALT_ON_MASK;
	}
	
	/**
	 * Turns off on-screen Alt.
	 */
	public void onScreenAltOff()	{
		onScreenMetaState = onScreenMetaState & ~ALT_ON_MASK;
	}

	/**
	 * Toggles on-screen Super mask.  Returns true if result is Super enabled, false otherwise.
	 * @return true if on false otherwise.
	 */
	public boolean onScreenSuperToggle() {
		// If we find Super on, turn it off. Otherwise, turn it on.
		if (onScreenMetaState == (onScreenMetaState | SUPER_ON_MASK)) {
			onScreenSuperOff();
			return false;
		}
		else {
			onScreenSuperOn();
			return true;
		}
	}
	
	/**
	 * Turns on on-screen Super.
	 */
	public void onScreenSuperOn()	{
		onScreenMetaState = onScreenMetaState | SUPER_ON_MASK;
	}	
	
	/**
	 * Turns off on-screen Super.
	 */
	public void onScreenSuperOff() {
		onScreenMetaState = onScreenMetaState & ~SUPER_ON_MASK;		
	}
	
	/**
	 * Toggles on-screen Shift mask.  Returns true if result is Shift enabled, false otherwise.
	 * @return true if on false otherwise.
	 */
	public boolean onScreenShiftToggle() {
		// If we find Super on, turn it off. Otherwise, turn it on.
		if (onScreenMetaState == (onScreenMetaState | SHIFT_ON_MASK)) {
			onScreenShiftOff();
			return false;
		}
		else {
			onScreenShiftOn();
			return true;
		}
	}
	
	/**
	 * Turns on on-screen Shift.
	 */
	public void onScreenShiftOn() {
		onScreenMetaState = onScreenMetaState | SHIFT_ON_MASK;
	}
	
	/**
	 * Turns off on-screen Shift.
	 */
	public void onScreenShiftOff() {
		onScreenMetaState = onScreenMetaState & ~SHIFT_ON_MASK;
	}
	
	/**
	 * Getter for the on-screen meta state.
	 * @return
	 */
	public int getMetaState () {
		return onScreenMetaState|lastDownMetaState;
	}
	
	/**
	 * Clears the on-screen meta state.
	 */
	public void clearOnScreenMetaState () {
		onScreenMetaState = 0;
	}

	/**
	 * Used to send a string over as a stream of unicode characters.
	 * @param s
	 */
	public void sendString(String s) {
		for (int i = 0; i < s.length(); i++) {
			char nextChar = s.charAt(i);
			if (Character.isISOControl(nextChar)) {
				if (nextChar == '\n') {
					int keyCode = KeyEvent.KEYCODE_ENTER;
					keyEvent(keyCode, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
					keyEvent(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
				}
			} else {
				sendUnicodeChar (nextChar);
			}
		}
	}
	
	/**
	 * Tries to convert a unicode character to a KeyEvent and if successful sends with keyEvent().
	 * @param unicodeChar
	 * @param metaState
	 */
	public boolean sendUnicodeChar (char unicodeChar) {
		KeyCharacterMap fullKmap    = KeyCharacterMap.load(KeyCharacterMap.FULL);
		KeyCharacterMap virtualKmap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
		char[] s = new char[1];
		s[0] = unicodeChar;
		KeyEvent[] events = fullKmap.getEvents(s);
		// Failing with the FULL keymap, try the VIRTUAL_KEYBOARD one.
		if (events == null) {
			events = virtualKmap.getEvents(s);
		}
		
        boolean result = false;
		if (events != null) {
			for (int i = 0; i < events.length; i++) {
				KeyEvent evt = events[i];
				keyEvent(evt.getKeyCode(), evt);
				result = true;
			}
		} else {
			android.util.Log.e("RemoteKeyboard", "Could not use any keymap to generate KeyEvent for unicode: " + unicodeChar);
		}
        return result;
	}
	
    /**
     * Used to calculate how many junk characters to skip.
     * @param numchars
     * @param evt
     * @return
     */
    int numJunkCharactersToSkip (int numchars, KeyEvent evt) {
        int i = 0;
        if (!skippedJunkChars) {
            if (numchars == 10000) {
                // We received the event not because the user typed something but
                // because of another reason (for example lock/unlock screen).
                i = numchars;
            } else {
                // The user has typed at least one char, so we need to skip just the junk
                // characters, so skip backward until we hit the first junk character.
                for (i = Math.max(numchars - 2, 0); i > 0 ; i--) {
                    if (evt.getCharacters().charAt(i) == '%') {
                        i = i + 1;
                        break;
                    }
                }
                skippedJunkChars = true;
            }
        }
        return i;
    }
    
    /**
     * Converts event meta state to our meta state.
     * @param event
     * @return
     */
    protected int convertEventMetaState (KeyEvent event, int eventMetaState) {
        int metaState = 0;
        int altMask = KeyEvent.META_ALT_RIGHT_ON;
        // Detect whether this event is coming from a default hardware keyboard.
        // We have to leave KeyEvent.KEYCODE_ALT_LEFT for symbol input on a default hardware keyboard.
        boolean defaultHardwareKbd = (event.getDeviceId() == 0);
        if (!bb && !defaultHardwareKbd) {
            altMask = KeyEvent.META_ALT_MASK;
        }
        
        // Add shift, ctrl, alt, and super to metaState if necessary.
        if ((eventMetaState & 0x000000c1 /*KeyEvent.META_SHIFT_MASK*/) != 0) {
            metaState |= SHIFT_ON_MASK;
        }
        if ((eventMetaState & 0x00007000 /*KeyEvent.META_CTRL_MASK*/) != 0) {
            metaState |= CTRL_ON_MASK;
        }
        if ((eventMetaState & altMask) !=0) {
            metaState |= ALT_ON_MASK;
        }
        if ((eventMetaState & 0x00070000 /*KeyEvent.META_META_MASK*/) != 0) {
            metaState |= SUPER_ON_MASK;
        }
        return metaState;
    }
}
